/*
 * Copyright 2018-2019 Kai Uwe Broulik <kde@privat.broulik.de>
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License as
 * published by the Free Software Foundation; either version 2 of
 * the License or (at your option) version 3 or any later version
 * accepted by the membership of KDE e.V. (or its successor approved
 * by the membership of KDE e.V.), which shall act as a proxy
 * defined in Section 14 of version 3 of the license.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>
 */

import QtQuick 2.10
import QtQuick.Layouts 1.1

import org.kde.plasma.plasmoid 2.0
import org.kde.plasma.core 2.0 as PlasmaCore
import org.kde.plasma.components 2.0 as PlasmaComponents
import org.kde.plasma.components 3.0 as PlasmaComponents3
import org.kde.plasma.extras 2.0 as PlasmaExtras
import org.kde.kirigami 2.11 as Kirigami

import org.kde.kcoreaddons 1.0 as KCoreAddons

import org.kde.notificationmanager 1.0 as NotificationManager

import "global"

ColumnLayout{
    // FIXME fix popup size when resizing panel smaller (so it collapses)
    //Layout.preferredWidth: units.gridUnit * 18
    //Layout.preferredHeight: units.gridUnit * 24
    //Layout.minimumWidth: units.gridUnit * 10
    //Layout.minimumHeight: units.gridUnit * 15
    Layout.fillHeight: plasmoid.formFactor === PlasmaCore.Types.Vertical

    spacing: units.smallSpacing

    // TODO these should be configurable in the future
    readonly property int dndMorningHour: 6
    readonly property int dndEveningHour: 20

    // HACK forward focus to the list
    onActiveFocusChanged: {
        if (activeFocus) {
            list.forceActiveFocus();
        }
    }

    Connections {
        target: plasmoid
        onExpandedChanged: {
            if (plasmoid.expanded) {
                list.positionViewAtBeginning();
                list.currentIndex = -1;
            }
        }
    }

    // header
    ColumnLayout {
        id: header
        visible: !Kirigami.Settings.isMobile
        Layout.fillWidth: true
        spacing: 0

        RowLayout {
            Layout.fillWidth: true
            spacing: 0

            RowLayout {
                id: dndRow
                spacing: units.smallSpacing
                enabled: NotificationManager.Server.valid

                PlasmaComponents3.CheckBox {
                    id: dndCheck
                    text: i18n("Do not disturb")
                    spacing: units.smallSpacing
                    checkable: true
                    checked: Globals.inhibited

                    // Let the menu open on press
                    onPressed: {
                        if (!Globals.inhibited) {
                            dndMenu.date = new Date();
                            // shows ontop of CheckBox to hide the fact that it's unchecked
                            // until you actually select something :)
                            dndMenu.open(0, 0);
                        }
                    }
                    // but disable only on click
                    onClicked: {
                        if (Globals.inhibited) {
                            Globals.revokeInhibitions();
                        }
                    }

                    contentItem: RowLayout {
                        spacing: dndCheck.spacing

                        PlasmaCore.IconItem {
                            Layout.leftMargin: dndCheck.mirrored ? 0 : dndCheck.indicator.width + dndCheck.spacing
                            Layout.rightMargin: dndCheck.mirrored ? dndCheck.indicator.width + dndCheck.spacing : 0
                            source: "notifications-disabled"
                            Layout.preferredWidth: units.iconSizes.smallMedium
                            Layout.preferredHeight: units.iconSizes.smallMedium
                        }

                        PlasmaComponents.Label {
                            text: i18n("Do not disturb")
                        }
                    }

                    PlasmaComponents.ModelContextMenu {
                        id: dndMenu
                        property date date
                        visualParent: dndCheck

                        onClicked: {
                            notificationSettings.notificationsInhibitedUntil = model.date;
                            notificationSettings.save();
                        }

                        model: {
                            var model = [];

                            // For 1 hour
                            var d = dndMenu.date;
                            d.setHours(d.getHours() + 1);
                            d.setSeconds(0);
                            model.push({date: d, text: i18n("For 1 hour")});

                            d = dndMenu.date;
                            d.setHours(d.getHours() + 4);
                            d.setSeconds(0);
                            model.push({date: d, text: i18n("For 4 hours")});

                            // Until this evening
                            if (dndMenu.date.getHours() < dndEveningHour) {
                                d = dndMenu.date;
                                // TODO make the user's preferred time schedule configurable
                                d.setHours(dndEveningHour);
                                d.setMinutes(0);
                                d.setSeconds(0);
                                model.push({date: d, text: i18n("Until this evening")});
                            }

                            // Until next morning
                            if (dndMenu.date.getHours() > dndMorningHour) {
                                d = dndMenu.date;
                                d.setDate(d.getDate() + 1);
                                d.setHours(dndMorningHour);
                                d.setMinutes(0);
                                d.setSeconds(0);
                                model.push({date: d, text: i18n("Until tomorrow morning")});
                            }

                            // Until Monday
                            // show Friday and Saturday, Sunday is "0" but for that you can use "until tomorrow morning"
                            if (dndMenu.date.getDay() >= 5) {
                                d = dndMenu.date;
                                d.setHours(dndMorningHour);
                                // wraps around if necessary
                                d.setDate(d.getDate() + (7 - d.getDay() + 1));
                                d.setMinutes(0);
                                d.setSeconds(0);
                                model.push({date: d, text: i18n("Until Monday")});
                            }

                            // Until "turned off"
                            d = dndMenu.date;
                            // Just set it to one year in the future so we don't need yet another "do not disturb enabled" property
                            d.setFullYear(d.getFullYear() + 1);
                            model.push({date: d, text: i18n("Until turned off")});

                            return model;
                        }
                    }
                }
            }

            Item {
                Layout.fillWidth: true
            }

            PlasmaComponents.ToolButton {
                iconName: "configure"
                // remove mnemonics
                tooltip: plasmoid.action("openKcm").text.replace(/([^&]*)&(.)([^&]*)/g, function (match, p1, p2, p3) {
                    return p1.concat(p2, p3);
                });
                visible: plasmoid.action("openKcm").enabled
                onClicked: plasmoid.action("openKcm").trigger()
            }

            PlasmaComponents.ToolButton {
                iconName: "edit-clear-history"
                tooltip: i18n("Clear History")
                enabled: plasmoid.action("clearHistory").visible
                onClicked: action_clearHistory()
            }
        }

        PlasmaExtras.DescriptiveLabel {
            Layout.leftMargin: dndCheck.mirrored ? 0 : dndCheck.indicator.width + 2 * dndCheck.spacing + units.iconSizes.smallMedium
            Layout.rightMargin: dndCheck.mirrored ? dndCheck.indicator.width + 2 * dndCheck.spacing + units.iconSizes.smallMedium : 0
            Layout.fillWidth: true
            wrapMode: Text.WordWrap
            textFormat: Text.PlainText
            text: {
                if (!Globals.inhibited) {
                    return "";
                }

                var inhibitedUntil = notificationSettings.notificationsInhibitedUntil;
                var inhibitedByApp = notificationSettings.notificationsInhibitedByApplication;
                var inhibitedByMirroredScreens = notificationSettings.inhibitNotificationsWhenScreensMirrored
                                                    && notificationSettings.screensMirrored;

                var sections = [];

                // Show until time if valid but not if too far int he future
                if (!isNaN(inhibitedUntil.getTime()) && inhibitedUntil.getTime() - new Date().getTime() < 365 * 24 * 60 * 60 * 1000 /* 1 year*/) {
                    sections.push(i18nc("Do not disturb until date", "Until %1",
                                        KCoreAddons.Format.formatRelativeDateTime(inhibitedUntil, Locale.ShortFormat)));
                }

                if (inhibitedByApp) {
                    var inhibitionAppNames = notificationSettings.notificationInhibitionApplications;
                    var inhibitionAppReasons = notificationSettings.notificationInhibitionReasons;

                    for (var i = 0, length = inhibitionAppNames.length; i < length; ++i) {
                        var name = inhibitionAppNames[i];
                        var reason = inhibitionAppReasons[i];

                        if (reason) {
                            sections.push(i18nc("Do not disturb until app has finished (reason)", "While %1 is active (%2)", name, reason));
                        } else {
                            sections.push(i18nc("Do not disturb until app has finished", "While %1 is active", name));
                        }
                    }
                }

                if (inhibitedByMirroredScreens) {
                    sections.push(i18nc("Do not disturb because external mirrored screens connected", "Screens are mirrored"))
                }

                return sections.join(" · ");
            }
            visible:  text !== ""
        }
    }

    PlasmaCore.SvgItem {
        visible: header.visible
        elementId: "horizontal-line"
        Layout.fillWidth: true
        // why is this needed here but not in the delegate?
        Layout.preferredHeight: naturalSize.height
        svg: PlasmaCore.Svg {
            id: lineSvg
            imagePath: "widgets/line"
        }
    }

    // actual notifications
    PlasmaExtras.ScrollArea {
        Layout.fillWidth: true
        Layout.fillHeight: true
        Layout.preferredWidth: units.gridUnit * 18
        Layout.preferredHeight: units.gridUnit * 24

        ListView {
            id: list
            model: historyModel
            currentIndex: -1

            Keys.onDeletePressed: {
                var idx = historyModel.index(currentIndex, 0);
                if (historyModel.data(idx, NotificationManager.Notifications.ClosableRole)) {
                    historyModel.close(idx);
                    // TODO would be nice to stay inside the current group when deleting an item
                }
            }
            Keys.onEnterPressed: Keys.onReturnPressed(event)
            Keys.onReturnPressed: {
                // Trigger default action, if any
                var idx = historyModel.index(currentIndex, 0);
                if (historyModel.data(idx, NotificationManager.Notifications.HasDefaultActionRole)) {
                    historyModel.invokeDefaultAction(idx);
                    return;
                }

                // Trigger thumbnail URL if there's one
                var urls = historyModel.data(idx, NotificationManager.Notifications.UrlsRole);
                if (urls && urls.length === 1) {
                    Qt.openUrlExternally(urls[0]);
                    historyModel.expire(idx);
                    return;
                }

                // TODO for finished jobs trigger "Open" or "Open Containing Folder" action
            }
            Keys.onLeftPressed: setGroupExpanded(currentIndex, LayoutMirroring.enabled)
            Keys.onRightPressed: setGroupExpanded(currentIndex, !LayoutMirroring.enabled)

            Keys.onPressed: {
                switch (event.key) {
                case Qt.Key_Home:
                    currentIndex = 0;
                    break;
                case Qt.Key_End:
                    currentIndex = count - 1;
                    break;
                }
            }

            function isRowExpanded(row) {
                var idx = historyModel.index(row, 0);
                return historyModel.data(idx, NotificationManager.Notifications.IsGroupExpandedRole);
            }

            function setGroupExpanded(row, expanded) {
                var rowIdx = historyModel.index(row, 0);
                var persistentRowIdx = historyModel.makePersistentModelIndex(rowIdx);
                var persistentGroupIdx = historyModel.makePersistentModelIndex(historyModel.groupIndex(rowIdx));

                historyModel.setData(rowIdx, expanded, NotificationManager.Notifications.IsGroupExpandedRole);

                // If the current item went away when the group collapsed, scroll to the group heading
                if (!persistentRowIdx || !persistentRowIdx.valid) {
                    if (persistentGroupIdx && persistentGroupIdx.valid) {
                        list.positionViewAtIndex(persistentGroupIdx.row, ListView.Contain);
                        // When closed via keyboard, also set a sane current index
                        if (list.currentIndex > -1) {
                            list.currentIndex = persistentGroupIdx.row;
                        }
                    }
                }
            }

            highlightMoveDuration: 0
            highlightResizeDuration: 0
            // Not using PlasmaComponents.Highlight as this is only for indicating keyboard focus
            highlight: PlasmaCore.FrameSvgItem {
                imagePath: "widgets/listitem"
                prefix: "pressed"
            }

            add: Transition {
                SequentialAnimation {
                    PropertyAction { property: "opacity"; value: 0 }
                    PauseAnimation { duration: units.longDuration }
                    ParallelAnimation {
                        NumberAnimation { property: "opacity"; from: 0; to: 1; duration: units.longDuration }
                        NumberAnimation { property: "height"; from: 0; duration: units.longDuration }
                    }
                }
            }
            addDisplaced: Transition {
                NumberAnimation { properties: "y"; duration:  units.longDuration }
            }

            remove: Transition {
                id: removeTransition
                ParallelAnimation {
                    NumberAnimation { property: "opacity"; to: 0; duration: units.longDuration }
                    NumberAnimation {
                        id: removeXAnimation
                        property: "x"
                        to: list.width
                        duration: units.longDuration
                    }
                }
            }
            removeDisplaced: Transition {
                SequentialAnimation {
                    PauseAnimation { duration: units.longDuration }
                    NumberAnimation { properties: "y"; duration:  units.longDuration }
                }
            }

            // This is so the delegates can detect the change in "isInGroup" and show a separator
            section {
                property: "isInGroup"
                criteria: ViewSection.FullString
            }

            delegate: DraggableDelegate {
                id: delegate
                width: list.width
                contentItem: delegateLoader

                draggable: !model.isGroup && model.type != NotificationManager.Notifications.JobType

                onDismissRequested: {
                    // Setting the animation target explicitly before removing the notification:
                    // Using ViewTransition.item.x to get the x position in the animation
                    // causes random crash in attached property access (cf. Bug 414066)
                    if (x < 0) {
                        removeXAnimation.to = -list.width;
                    }

                    historyModel.close(historyModel.index(index, 0));
                }

                Loader {
                    id: delegateLoader
                    width: list.width
                    sourceComponent: model.isGroup ? groupDelegate : notificationDelegate

                    Component {
                        id: groupDelegate
                        NotificationHeader {
                            applicationName: model.applicationName
                            applicationIconSource: model.applicationIconName
                            originName: model.originName || ""

                            // don't show timestamp for group

                            configurable: model.configurable
                            closable: model.closable
                            closeButtonTooltip: i18n("Close Group")

                            onCloseClicked: {
                                historyModel.close(historyModel.index(index, 0))
                                if (list.count === 0) {
                                    root.closePassivePlasmoid();
                                }
                            }

                            onConfigureClicked: historyModel.configure(historyModel.index(index, 0))
                        }
                    }

                    Component {
                        id: notificationDelegate
                        ColumnLayout {
                            spacing: units.smallSpacing

                            RowLayout {
                                Item {
                                    id: groupLineContainer
                                    Layout.fillHeight: true
                                    Layout.topMargin: units.smallSpacing
                                    width: units.iconSizes.small
                                    visible: model.isInGroup

                                    PlasmaCore.SvgItem {
                                        elementId: "vertical-line"
                                        svg: lineSvg
                                        anchors.horizontalCenter: parent.horizontalCenter
                                        width: units.iconSizes.small
                                        height: parent.height
                                    }
                                }

                                NotificationItem {
                                    Layout.fillWidth: true

                                    notificationType: model.type

                                    inGroup: model.isInGroup

                                    applicationName: model.applicationName
                                    applicationIconSource: model.applicationIconName
                                    originName: model.originName || ""

                                    time: model.updated || model.created

                                    // configure button on every single notifications is bit overwhelming
                                    configurable: !inGroup && model.configurable

                                    dismissable: model.type === NotificationManager.Notifications.JobType
                                        && model.jobState !== NotificationManager.Notifications.JobStateStopped
                                        && model.dismissed
                                        // TODO would be nice to be able to undismiss jobs even when they autohide
                                        && notificationSettings.permanentJobPopups
                                    dismissed: model.dismissed || false
                                    closable: model.closable

                                    summary: model.summary
                                    body: model.body || ""
                                    icon: model.image || model.iconName

                                    urls: model.urls || []

                                    jobState: model.jobState || 0
                                    percentage: model.percentage || 0
                                    jobError: model.jobError || 0
                                    suspendable: !!model.suspendable
                                    killable: !!model.killable
                                    jobDetails: model.jobDetails || null

                                    configureActionLabel: model.configureActionLabel || ""
                                    // In the popup the default action is triggered by clicking on the popup
                                    // however in the list this is undesirable, so instead show a clickable button
                                    // in case you have a non-expired notification in history (do not disturb mode)
                                    // unless it has the same label as an action
                                    readonly property bool addDefaultAction: (model.hasDefaultAction
                                                                            && model.defaultActionLabel
                                                                            && (model.actionLabels || []).indexOf(model.defaultActionLabel) === -1) ? true : false
                                    actionNames: {
                                        var actions = (model.actionNames || []);
                                        if (addDefaultAction) {
                                            actions.unshift("default"); // prepend
                                        }
                                        return actions;
                                    }
                                    actionLabels: {
                                        var labels = (model.actionLabels || []);
                                        if (addDefaultAction) {
                                            labels.unshift(model.defaultActionLabel);
                                        }
                                        return labels;
                                    }

                                    onCloseClicked: {
                                        historyModel.close(historyModel.index(index, 0));
                                        if (list.count === 0) {
                                            root.closePassivePlasmoid();
                                        }
                                    }
                                    onDismissClicked: {
                                        model.dismissed = false;
                                        root.closePassivePlasmoid();
                                    }
                                    onConfigureClicked: historyModel.configure(historyModel.index(index, 0))

                                    onActionInvoked: {
                                        if (actionName === "default") {
                                            historyModel.invokeDefaultAction(historyModel.index(index, 0));
                                        } else {
                                            historyModel.invokeAction(historyModel.index(index, 0), actionName);
                                        }
                                        // Keep it in the history
                                        historyModel.expire(historyModel.index(index, 0));
                                    }
                                    onOpenUrl: {
                                        Qt.openUrlExternally(url);
                                        historyModel.expire(historyModel.index(index, 0));
                                    }
                                    onFileActionInvoked: historyModel.expire(historyModel.index(index, 0))

                                    onSuspendJobClicked: historyModel.suspendJob(historyModel.index(index, 0))
                                    onResumeJobClicked: historyModel.resumeJob(historyModel.index(index, 0))
                                    onKillJobClicked: historyModel.killJob(historyModel.index(index, 0))
                                }
                            }

                            PlasmaComponents.ToolButton {
                                Layout.preferredWidth: minimumWidth
                                iconName: model.isGroupExpanded ? "arrow-up" : "arrow-down"
                                text: model.isGroupExpanded ? i18n("Show Fewer")
                                                            : i18nc("Expand to show n more notifications",
                                                                    "Show %1 More", (model.groupChildrenCount - model.expandedGroupChildrenCount))
                                visible: (model.groupChildrenCount > model.expandedGroupChildrenCount || model.isGroupExpanded)
                                    && delegate.ListView.nextSection !== delegate.ListView.section
                                onClicked: list.setGroupExpanded(model.index, !model.isGroupExpanded)
                            }

                            PlasmaCore.SvgItem {
                                Layout.fillWidth: true
                                Layout.bottomMargin: units.smallSpacing
                                elementId: "horizontal-line"
                                svg: lineSvg

                                // property is only atached to the delegate itself (the Loader in our case)
                                visible: (!model.isInGroup || delegate.ListView.nextSection !== delegate.ListView.section)
                                                && delegate.ListView.nextSection !== "" // don't show after last item
                            }
                        }
                    }
                }
            }

            PlasmaExtras.Heading {
                anchors.fill: parent
                horizontalAlignment: Text.AlignHCenter
                verticalAlignment: Text.AlignVCenter
                wrapMode: Text.WordWrap
                level: 3
                text: i18n("No unread notifications.")
                visible: list.count === 0 && NotificationManager.Server.valid
                enabled: false
            }

            ColumnLayout {
                id: serverUnavailableColumn

                width: list.width
                visible: list.count === 0 && !NotificationManager.Server.valid

                PlasmaExtras.Heading {
                    Layout.fillWidth: true
                    level: 3
                    opacity: 0.6
                    text: i18n("Notification service not available")
                    wrapMode: Text.WordWrap
                }

                PlasmaComponents.Label {
                    // Checking valid to avoid creating ServerInfo object if everything is alright
                    readonly property NotificationManager.ServerInfo currentOwner: !NotificationManager.Server.valid ? NotificationManager.Server.currentOwner
                                                                                                                     : null

                    Layout.fillWidth: true
                    wrapMode: Text.WordWrap
                    text: currentOwner ? i18nc("Vendor and product name",
                                         "Notifications are currently provided by '%1 %2'",
                                         currentOwner.vendor,
                                         currentOwner.name)
                                       : ""
                    visible: currentOwner && currentOwner.vendor && currentOwner.name
                }
            }
        }
    }
}
